# -*- coding: utf-8 -*-

import re
import shutil
import sys
import os
import json
import time
import base64

from PyQt6.QtWidgets import (
    QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QGridLayout,
    QTabWidget, QGroupBox, QLabel, QLineEdit, QPushButton, QSpinBox, QComboBox,
    QProgressBar, QTextEdit, QTableWidget, QTableWidgetItem, QHeaderView,
    QFileDialog, QMessageBox, QAbstractItemView
)
from PyQt6.QtCore import (
    Qt, QThread, pyqtSignal, pyqtSlot, QObject, QUrl, QModelIndex
)
from PyQt6.QtGui import QIcon, QPixmap, QDesktopServices

import ollama

# --- 全局常量 ---
JSONL_FILE = "image_tags.jsonl"
SUPPORTED_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.bmp', '.gif', '.tiff', '.webp']
OLLAMA_HOST = 'http://localhost:11434'

# --- 样式表 ---
STYLESHEET = """
* {
    font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
    font-size: 10pt;
    color: #000000;
}
QMainWindow, QWidget {
    background-color: #f4f6f8;
}
QTabWidget::pane { 
    border-top: 2px solid #28a745; 
    background-color: #ffffff;
}
QTabBar::tab {
    background: #f0f0f0; border: 1px solid #dcdcdc; border-bottom: none;
    padding: 10px 25px; margin-right: 2px;
    border-top-left-radius: 6px; border-top-right-radius: 6px;
    color: #555555;
}
QTabBar::tab:selected {
    background: #ffffff; font-weight: bold; color: #000000;
    border-color: #28a745;
}
QTabBar::tab:hover {
    background: #eaf6ec;
}
QGroupBox {
    background-color: #ffffff; border: 1px solid #dcdfe6;
    border-radius: 8px; margin-top: 1ex; font-weight: bold; padding: 15px;
}
QGroupBox::title {
    subcontrol-origin: margin; subcontrol-position: top left;
    padding: 0 10px; margin-left: 10px;
}
QPushButton {
    background-color: #fdfdfd; border: 1px solid #c4c4c4;
    padding: 8px 16px; border-radius: 5px; color: #333333;
    min-width: 80px;
}
QPushButton:hover {
    background-color: #f0fff4; border-color: #28a745;
}
QPushButton:pressed {
    background-color: #e0f2e5;
}
QPushButton#AccentButton {
    background-color: #28a745; color: white;
    font-weight: bold; border: none;
}
QPushButton#AccentButton:hover { background-color: #228c3a; }
QPushButton#AccentButton:pressed { background-color: #1e7a33; }
QLineEdit, QSpinBox, QComboBox {
    padding: 8px; border: 1px solid #dcdfe6;
    border-radius: 5px; background-color: #ffffff;
}
QTableWidget {
    border: 1px solid #dcdfe6; gridline-color: #e8e8e8;
    background-color: white; alternate-background-color: #f9fbf9;
    selection-background-color: #eaf6ec; selection-color: #000000;
}
QHeaderView::section {
    background-color: #f2f2f2; padding: 8px; border: none;
    border-bottom: 1px solid #dcdfe6; font-weight: bold;
}
QProgressBar {
    border: 1px solid #dcdfe6; border-radius: 5px; text-align: center;
    background-color: #ffffff; height: 28px; font-weight: bold; color: #000000;
}
QProgressBar::chunk { background-color: #28a745; border-radius: 4px; }
QTextEdit {
    border: 1px solid #dcdfe6; border-radius: 5px; background-color: white;
}
QLabel#HeaderLabel {
    font-size: 20pt; font-weight: bold; color: #2c3e50; padding: 20px 10px;
}
"""

# --- Worker 线程 ---
class Worker(QObject):
    finished = pyqtSignal()
    progress = pyqtSignal(int, int)
    log = pyqtSignal(str)
    image_processed = pyqtSignal(str, str, list)

    def __init__(self, image_paths, tag_count, speed, model_name, client):
        super().__init__()
        self.image_paths = image_paths
        self.tag_count = tag_count
        self.speed = speed
        self._is_running = True
        self.model_name = model_name
        self.client = client

    @pyqtSlot()
    def run(self):
        total = len(self.image_paths)
        for i, path in enumerate(self.image_paths):
            if not self._is_running:
                self.log.emit("处理已由用户停止。")
                break
            
            self.log.emit(f"({i+1}/{total}) 正在处理: {os.path.basename(path)}")
            
            try:
                tags = self.generate_tags(path)
                self.image_processed.emit(path, os.path.basename(path), tags)
                self.log.emit(f"处理成功: {os.path.basename(path)} -> 标签: {', '.join(tags)}")
                self.progress.emit(i + 1, total) # 成功后更新进度
            except Exception as e:
                self.log.emit(f"处理失败: {os.path.basename(path)} - 错误: {str(e)}")
                self.progress.emit(i + 1, total) # 失败后也更新进度，防止卡住

            if self.speed == "慢速": time.sleep(2)
            elif self.speed == "中等": time.sleep(1)
        
        self.log.emit(f"本轮任务执行完毕。")
        self.finished.emit()

    def stop(self):
        self._is_running = False

    def generate_tags(self, image_path):
        try:
            with open(image_path, "rb") as image_file:
                base64_image = base64.b64encode(image_file.read()).decode('utf-8')
            
            prompt = (
                f"请为这张图片生成{self.tag_count}个最相关的描述性标签。"
                "你必须只返回用单个逗号分隔的关键词列表，不要包含任何数字、序号、点、解释或多余的文字。"
                "例如：'天空,云,山脉,日落,自然'。"
            )
            
            response = self.client.chat(model=self.model_name, messages=[{'role': 'user', 'content': prompt, 'images': [base64_image]}])
            tags_str = response['message']['content'].strip()

            # 第一步: 移除所有 "1.", "2、" 等数字序号格式
            cleaned_str = re.sub(r'\d+\s*[.、]\s*', '', tags_str)
            
            # 第二步: 在清理过的字符串上进行分割
            tags = [tag.strip() for tag in re.split(r'[,\，、\s]+', cleaned_str) if tag.strip()]
            
            return tags[:self.tag_count] if len(tags) > self.tag_count else tags
        except Exception as e:
            self.log.emit(f"生成标签时出现严重错误: {str(e)}")
            raise e

# --- 主窗口类 ---
class ImageTaggerApp(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("智能图片分类器")
        self.setGeometry(100, 100, 1200, 800)
        self.setStyleSheet(STYLESHEET)
        
        icon_path = "icon.ico"
        if os.path.exists(icon_path):
            self.setWindowIcon(QIcon(icon_path))

        self.tag_data = {}
        self.current_image_path = ""
        self.thread = None
        self.worker = None
        self.client = ollama.Client(host=OLLAMA_HOST, timeout=30)

        self.setup_ui()
        self.log_message("欢迎使用智能图片分类器！")
        self.load_tag_data()
        self.check_model_availability()

    def setup_ui(self):
        # ... (UI setup code remains unchanged)
        main_widget = QWidget()
        self.setCentralWidget(main_widget)
        main_layout = QVBoxLayout(main_widget)
        main_layout.setContentsMargins(20, 10, 20, 20)
        header_label = QLabel("智能图片分类器", objectName="HeaderLabel")
        header_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        main_layout.addWidget(header_label)
        self.tabs = QTabWidget()
        main_layout.addWidget(self.tabs)
        self.add_tab = QWidget()
        self.search_tab = QWidget()
        self.image_search_tab = QWidget()
        self.tabs.addTab(self.add_tab, "图片处理")
        self.tabs.addTab(self.search_tab, "标签搜索")
        self.tabs.addTab(self.image_search_tab, "图片分析与相似搜索")
        self.setup_add_tab()
        self.setup_search_tab()
        self.setup_image_search_tab()
        self.statusBar().showMessage("就绪")
    
    def setup_add_tab(self):
        # ... (UI setup code remains unchanged)
        layout = QVBoxLayout(self.add_tab)
        layout.setContentsMargins(15, 15, 15, 15)
        folder_group = QGroupBox("第一步: 选择图片文件夹")
        folder_layout = QHBoxLayout()
        self.folder_path_edit = QLineEdit()
        self.folder_path_edit.setPlaceholderText("请选择包含图片的文件夹...")
        browse_btn = QPushButton("浏览...")
        browse_btn.clicked.connect(self.browse_folder)
        folder_layout.addWidget(QLabel("文件夹路径:"))
        folder_layout.addWidget(self.folder_path_edit)
        folder_layout.addWidget(browse_btn)
        folder_group.setLayout(folder_layout)
        layout.addWidget(folder_group)
        panel_group = QGroupBox("第二步: 设置并开始")
        grid_layout = QGridLayout()
        grid_layout.setSpacing(15)
        grid_layout.addWidget(QLabel("视觉模型:"), 0, 0)
        self.model_combo = QComboBox()
        grid_layout.addWidget(self.model_combo, 0, 1, 1, 2)
        grid_layout.addWidget(QLabel("每图标签数:"), 1, 0)
        self.tag_count_spin = QSpinBox()
        self.tag_count_spin.setRange(1, 20)
        self.tag_count_spin.setValue(5)
        grid_layout.addWidget(self.tag_count_spin, 1, 1, 1, 2)
        grid_layout.addWidget(QLabel("处理速度:"), 2, 0)
        self.speed_combo = QComboBox()
        self.speed_combo.addItems(["快速", "中等", "慢速"])
        self.speed_combo.setCurrentText("中等")
        grid_layout.addWidget(self.speed_combo, 2, 1, 1, 2)
        self.progress_bar = QProgressBar()
        self.progress_bar.setValue(0)
        grid_layout.addWidget(self.progress_bar, 3, 0, 1, 3)
        self.progress_label = QLabel("等待开始...")
        self.progress_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        grid_layout.addWidget(self.progress_label, 4, 0, 1, 3)
        self.start_btn = QPushButton("开始处理", objectName="AccentButton")
        self.stop_btn = QPushButton("停止")
        self.start_btn.clicked.connect(self.start_processing)
        self.stop_btn.clicked.connect(self.stop_processing)
        self.stop_btn.setEnabled(False)
        button_layout = QHBoxLayout()
        button_layout.addStretch()
        button_layout.addWidget(self.start_btn)
        button_layout.addWidget(self.stop_btn)
        button_layout.addStretch()
        grid_layout.addLayout(button_layout, 5, 0, 1, 3)
        panel_group.setLayout(grid_layout)
        layout.addWidget(panel_group)
        layout.addStretch()

    def setup_search_tab(self):
        # ... (UI setup code remains unchanged)
        layout = QVBoxLayout(self.search_tab)
        layout.setContentsMargins(15, 15, 15, 15)
        search_layout = QHBoxLayout()
        self.search_entry = QLineEdit()
        self.search_entry.setPlaceholderText("输入标签关键词搜索，按Enter键开始...")
        self.search_entry.returnPressed.connect(self.search_tags)
        search_btn = QPushButton("搜索")
        search_btn.clicked.connect(self.search_tags)
        clear_btn = QPushButton("清空")
        clear_btn.clicked.connect(self.clear_search)
        search_layout.addWidget(QLabel("搜索标签:"))
        search_layout.addWidget(self.search_entry, 1)
        search_layout.addWidget(search_btn)
        search_layout.addWidget(clear_btn)
        layout.addLayout(search_layout)
        self.result_table = QTableWidget()
        self.result_table.setColumnCount(2)
        self.result_table.setHorizontalHeaderLabels(["图片名称", "标签"])
        self.result_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Interactive)
        self.result_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
        self.result_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
        self.result_table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
        self.result_table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
        self.result_table.setAlternatingRowColors(True)
        self.result_table.doubleClicked.connect(self.open_image_with_system)
        layout.addWidget(self.result_table)

    def setup_image_search_tab(self):
        layout = QVBoxLayout(self.image_search_tab)
        layout.setContentsMargins(15, 15, 15, 15)
        top_layout = QHBoxLayout()
        img_group = QGroupBox("手动选择图片进行分析")
        img_layout = QVBoxLayout()
        self.image_label = QLabel("手动选择图片以进行分析")
        self.image_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        self.image_label.setMinimumSize(250, 250)
        self.image_label.setStyleSheet("border: 1px dashed #dcdfe6; border-radius: 8px;")
        select_img_btn = QPushButton("选择图片...")
        select_img_btn.clicked.connect(self.select_search_image)
        img_layout.addWidget(self.image_label, 1)
        img_layout.addWidget(select_img_btn, 0, Qt.AlignmentFlag.AlignCenter)
        img_group.setLayout(img_layout)
        top_layout.addWidget(img_group)
        tag_group = QGroupBox("分析结果与相似搜索")
        tag_layout = QVBoxLayout()
        self.image_tags_text = QTextEdit()
        self.image_tags_text.setReadOnly(True)
        self.search_similar_btn = QPushButton("搜索相似图片", objectName="AccentButton")
        self.search_similar_btn.setEnabled(False)
        self.search_similar_btn.clicked.connect(self.search_similar_images)
        tag_layout.addWidget(QLabel("分析出的标签:"))
        tag_layout.addWidget(self.image_tags_text, 1)
        tag_layout.addWidget(self.search_similar_btn, 0, Qt.AlignmentFlag.AlignCenter)
        tag_group.setLayout(tag_layout)
        top_layout.addWidget(tag_group, 1)
        layout.addLayout(top_layout)
        similar_group = QGroupBox("相似图片结果")
        similar_layout = QVBoxLayout()
        self.similar_table = QTableWidget()
        self.similar_table.setColumnCount(3)
        self.similar_table.setHorizontalHeaderLabels(["图片名称", "相似度", "标签"])
        self.similar_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Interactive)
        self.similar_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Fixed)
        self.similar_table.setColumnWidth(1, 100)
        self.similar_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch)
        self.similar_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
        self.similar_table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
        self.similar_table.setAlternatingRowColors(True)
        self.similar_table.doubleClicked.connect(self.open_image_with_system)
        similar_layout.addWidget(self.similar_table)
        similar_group.setLayout(similar_layout)
        layout.addWidget(similar_group, 1)

    @pyqtSlot(str)
    def log_message(self, message):
        timestamp = time.strftime('%H:%M:%S')
        log_entry = f"[{timestamp}] {message}"
        print(log_entry)
        self.statusBar().showMessage(message, 5000)
    
    def is_vision_model(self, model_name: str) -> bool:
        name_lower = model_name.lower()
        vision_keywords = ['vl', 'vision', 'llava']
        return any(keyword in name_lower for keyword in vision_keywords)

    def check_model_availability(self):
        self.log_message("正在检查Ollama模型...")
        try:
            models_data = self.client.list()      
            all_models = models_data.get('models', [])
            vision_models = [m.model for m in all_models if self.is_vision_model(m.model)]
            self.log_message(f"检测到 {len(vision_models)} 个可能的视觉模型。")
            self.model_combo.clear()
            if vision_models:
                self.model_combo.addItems(vision_models)
                self.log_message("视觉模型列表已更新。")
            else:
                self.log_message("警告: 未找到可用的视觉模型。")
                QMessageBox.warning(self, "无视觉模型", "未在Ollama中找到任何可识别的视觉模型。")
        except Exception as e:
            self.log_message(f"错误: 无法连接到Ollama服务 - {e}")
            QMessageBox.critical(self, "Ollama连接错误", f"错误: 无法连接到Ollama服务 - {e}")

    @pyqtSlot()
    def browse_folder(self):
        folder = QFileDialog.getExistingDirectory(self, "选择文件夹")
        if folder:
            self.folder_path_edit.setText(folder)
            self.log_message(f"已选择文件夹: {folder}")
    
    @pyqtSlot()
    def start_processing(self):
        selected_model = self.model_combo.currentText()
        if not selected_model:
            QMessageBox.warning(self, "无模型", "请先在“选项”中选择一个视觉模型。")
            return
            
        folder = self.folder_path_edit.text()
        if not folder or not os.path.isdir(folder):
            QMessageBox.warning(self, "警告", "请选择一个有效的文件夹。")
            return

        self.log_message(f"开始扫描文件夹: {folder}")
        all_found_paths = [os.path.join(root, file) for root, _, files in os.walk(folder) for file in files if file.lower().endswith(tuple(SUPPORTED_EXTENSIONS))]
        self.log_message(f"发现 {len(all_found_paths)} 个支持的图片文件。")

        if not all_found_paths:
            QMessageBox.information(self, "无图片", "所选文件夹中未找到支持的图片文件。")
            return

        requested_tag_count = self.tag_count_spin.value()
        paths_to_process = []
        
        for path in all_found_paths:
            if path not in self.tag_data:
                paths_to_process.append(path) # 新文件，直接处理
            else:
                existing_tag_count = len(self.tag_data[path].get('tags', []))
                if existing_tag_count != requested_tag_count:
                    self.log_message(f"需要更新: {os.path.basename(path)} (请求 {requested_tag_count} 个标签, 已有 {existing_tag_count} 个)")
                    paths_to_process.append(path) # 标签数量要求变化，需要重新处理
                else:
                    self.log_message(f"跳过: {os.path.basename(path)} (已有 {existing_tag_count} 个标签，与请求一致)")

        if not paths_to_process:
            self.log_message("所有图片均已处理且满足当前要求。")
            QMessageBox.information(self, "无需操作", "所有图片均已处理且满足当前的标签数量要求。")
            return

        self.log_message(f"准备使用模型 '{selected_model}' 处理/更新 {len(paths_to_process)} 张图片。")
        self.start_btn.setEnabled(False)
        self.stop_btn.setEnabled(True)
        self.log_message("--- 开始批量处理 ---")

        self.thread = QThread()
        self.worker = Worker(paths_to_process, requested_tag_count, self.speed_combo.currentText(), selected_model, self.client)
        self.worker.moveToThread(self.thread)

        self.thread.started.connect(self.worker.run)
        self.worker.finished.connect(self.thread.quit)
        self.worker.finished.connect(self.worker.deleteLater)
        self.thread.finished.connect(self.thread.deleteLater)
        self.worker.log.connect(self.log_message)
        self.worker.progress.connect(self.update_progress)
        self.worker.image_processed.connect(self.save_tag_data_in_memory)
        
        self.worker.finished.connect(self.rewrite_database_file)
        self.thread.finished.connect(lambda: (self.start_btn.setEnabled(True), self.stop_btn.setEnabled(False), self.log_message("--- 批量处理结束 ---")))
        
        self.thread.start()

    @pyqtSlot()
    def stop_processing(self):
        if self.worker: self.worker.stop()
        self.log_message("正在发送停止信号...")
        self.stop_btn.setEnabled(False)
    
    @pyqtSlot(int, int)
    def update_progress(self, current, total):
        if total > 0:
            self.progress_bar.setMaximum(total)
            self.progress_bar.setValue(current)
            self.progress_label.setText(f"{current} / {total}")
        if current == total:
            self.progress_label.setText("处理完成！")

    @pyqtSlot(str, str, list)
    def save_tag_data_in_memory(self, image_path, image_name, tags):
        """仅更新内存中的数据字典。"""
        entry = {"image_path": image_path, "image_name": image_name, "tags": tags}
        self.tag_data[image_path] = entry

    @pyqtSlot()
    def rewrite_database_file(self):
        """将内存中的所有数据一次性重写到 .jsonl 文件中。"""
        self.log_message("所有处理已完成，正在更新数据库文件...")
        try:
            with open(JSONL_FILE, 'w', encoding='utf-8') as f:
                for entry in self.tag_data.values():
                    f.write(json.dumps(entry, ensure_ascii=False) + '\n')
            self.log_message(f"数据库文件 '{JSONL_FILE}' 已成功更新，包含 {len(self.tag_data)} 条记录。")
        except IOError as e:
            self.log_message(f"严重错误：无法写入数据库文件 {JSONL_FILE}: {e}")
            QMessageBox.critical(self, "文件写入错误", f"无法更新数据库文件:\n{e}")

    def load_tag_data(self):
        """从 JSONL 文件加载标签数据到字典中。"""
        if os.path.exists(JSONL_FILE):
            try:
                with open(JSONL_FILE, 'r', encoding='utf-8') as f:
                    for line in f:
                        if line.strip():
                            entry = json.loads(line.strip())
                            # 使用图片路径作为键
                            self.tag_data[entry['image_path']] = entry
                self.log_message(f"成功加载 {len(self.tag_data)} 条本地标签数据。")
            except (IOError, json.JSONDecodeError) as e:
                self.log_message(f"错误: 无法加载或解析 {JSONL_FILE}: {e}")
                backup_file = f"{JSONL_FILE}.{int(time.time())}.bak"
                shutil.copy(JSONL_FILE, backup_file)
                self.log_message(f"重要: 数据文件已损坏，已创建备份: {backup_file}")
        else:
            self.log_message(f"未找到数据文件。将在处理后自动创建 {JSONL_FILE}。")
    
    @pyqtSlot()
    def search_tags(self):
        query = self.search_entry.text().strip().lower()
        if not query: return
        self.log_message(f"开始标签搜索, 关键词: '{query}'")
        self.result_table.setRowCount(0)
        # 从字典的值中搜索
        results = [entry for entry in self.tag_data.values() if any(query in tag.lower() for tag in entry.get('tags', []))]
        for entry in results:
            row_position = self.result_table.rowCount()
            self.result_table.insertRow(row_position)
            self.result_table.setItem(row_position, 0, QTableWidgetItem(entry['image_name']))
            self.result_table.setItem(row_position, 1, QTableWidgetItem(", ".join(entry.get('tags', []))))
        self.log_message(f"搜索完毕，找到 {len(results)} 个结果。")

    @pyqtSlot()
    def clear_search(self):
        self.search_entry.clear()
        self.result_table.setRowCount(0)
        self.log_message("搜索结果已清空。")

    @pyqtSlot()
    def select_search_image(self):
        file_path, _ = QFileDialog.getOpenFileName(self, "选择图片", "", f"图片文件 ({' '.join(['*' + ext for ext in SUPPORTED_EXTENSIONS])})")
        if file_path:
            self.log_message(f"已选择待分析图片: {file_path}")
            self.current_image_path = file_path
            self.display_image(file_path)
            self.analyze_image(file_path)
            self.search_similar_btn.setEnabled(True)

    def display_image(self, image_path):
        pixmap = QPixmap(image_path)
        scaled_pixmap = pixmap.scaled(self.image_label.size(), Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)
        self.image_label.setPixmap(scaled_pixmap)

    def analyze_image(self, image_path):
        selected_model = self.model_combo.currentText()
        if not selected_model:
            QMessageBox.warning(self, "无模型", "请先在“选项”中选择一个视觉模型。")
            return
        self.log_message(f"开始使用模型 '{selected_model}' 分析单个图片: {os.path.basename(image_path)}...")
        self.image_tags_text.setText("正在分析中...")
        analyzer_thread = QThread()
        analyzer = Worker([image_path], self.tag_count_spin.value(), "快速", selected_model, self.client)
        analyzer.moveToThread(analyzer_thread)
        analyzer_thread.started.connect(analyzer.run)
        analyzer.log.connect(self.log_message)
        analyzer.image_processed.connect(lambda _, __, tags: self.image_tags_text.setText(", ".join(tags)))
        analyzer.finished.connect(lambda: (analyzer_thread.quit(), analyzer.deleteLater(), self.log_message("单张图片分析完成。")))
        analyzer_thread.start()

    @pyqtSlot()
    def search_similar_images(self):
        tags_text = self.image_tags_text.toPlainText().strip()
        if not tags_text or tags_text == "正在分析中...":
            QMessageBox.information(self, "无法搜索", "当前图片没有可用于搜索的标签。")
            return
        current_tags_set = {t.strip() for t in re.split(r'[,\，、\s]+', tags_text) if t.strip()}
        self.log_message(f"开始搜索相似图片，基于标签: {tags_text}")
        self.similar_table.setRowCount(0)
        results = []
        for entry in self.tag_data.values():
            if entry['image_path'] == self.current_image_path: continue
            entry_tags_set = set(entry.get('tags', []))
            common_tags = current_tags_set.intersection(entry_tags_set)
            union_tags = current_tags_set.union(entry_tags_set)
            similarity = len(common_tags) / len(union_tags) * 100 if union_tags else 0
            if similarity > 0: results.append({"entry": entry, "similarity": similarity})
        results.sort(key=lambda x: x['similarity'], reverse=True)
        for res in results:
            entry = res['entry']
            similarity = res['similarity']
            row_position = self.similar_table.rowCount()
            self.similar_table.insertRow(row_position)
            self.similar_table.setItem(row_position, 0, QTableWidgetItem(entry['image_name']))
            sim_item = QTableWidgetItem(f"{similarity:.1f}%")
            sim_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
            self.similar_table.setItem(row_position, 1, sim_item)
            self.similar_table.setItem(row_position, 2, QTableWidgetItem(", ".join(entry.get('tags', []))))
        self.log_message(f"相似图片搜索完毕，找到 {len(results)} 个结果。")

    @pyqtSlot(QModelIndex)
    def open_image_with_system(self, model_index):
        table = self.sender()
        if not table or not model_index.isValid(): return
        row = model_index.row()
        if row < 0 or table.item(row, 0) is None: return
        image_name = table.item(row, 0).text()
        image_path = ""
        # 从字典的值中查找
        for entry in self.tag_data.values():
            if entry['image_name'] == image_name:
                image_path = entry['image_path']
                break
        self.log_message(f"请求使用系统默认应用打开图片: '{image_path}'")
        if image_path and os.path.exists(image_path):
            QDesktopServices.openUrl(QUrl.fromLocalFile(image_path))
        else:
            self.log_message(f"错误: 无法找到文件路径 '{image_path}'")
            QMessageBox.warning(self, "文件未找到", f"无法找到文件: {image_name}")

    def closeEvent(self, event):
        if self.thread and self.thread.isRunning():
            reply = QMessageBox.question(self, '仍在处理中', '图片仍在处理中，确定要退出吗？',
                                           QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
                                           QMessageBox.StandardButton.No)
            if reply == QMessageBox.StandardButton.Yes:
                self.log_message("用户选择退出，正在停止处理线程...")
                self.stop_processing()
                self.thread.quit()
                self.thread.wait(3000)
                event.accept()
            else:
                event.ignore()
        else:
            self.log_message("应用程序关闭。")
            event.accept()

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = ImageTaggerApp()
    window.show()
    sys.exit(app.exec())